########################################################################################################
# PowerDbg v5.2
#
# New Features for 5.2:
#
# - Easy installation.
# - Just one file.
# - Faster, results are cached in memory not using POWERDBG.LOG anymore.
#
# The new changes are based on suggestions from:
# Brad Linscott - Premier Field Engineer
# Lee Holmes    - Software Development Engineer - http://www.leeholmes.com/blog/default.aspx
#
# Note: PowerDbg v5.2 breaks compatibility with some of my previous scripts, however, they were already changed
#       and compatiblized. You just have to download them again.
#       They are:
#       - PowerDbgScriptDumpDict.ps1
#       - PowerDbgScriptExceptions.ps1
#
# PowerDbg v 5.1
#
# New Features for 5.1:
#
# - Load-PowerDbgSymbols
# - Parse-PowerDbgASPXPAGES
# - Parse-PowerDbgCLRSTACK
# - Parse-PowerDbgTHREADS
# - Parse-PowerDbgDSO
#
# New Features for 5.0:
#
# - No more SendKeys communication! Now PowerDbg uses the approach created by Lee Holmes:
#   http://www.leeholmes.com/blog/ScriptingWinDbgWithPowerShell.aspx
# - Easier to use and much more performance.
#
# New Features for 4.0:
#
# - Send-PowerDbgCTRL-BREAK
# - Parse-PowerDbgGCHANDLELEAKS
# - Parse-PowerDbgDUMPOBJ
# - Parse-PowerDbgDD-L1
# - Send-PowerDbgResumeExecution
# - Increased default timeout to 1 hour
# - Fixed three subtles bug in Send-PowerDbgCommand.
#
# New Features for 3.1:
#
# - Parse-PowerDbgPRINTEXCEPTION
# - No more Sleep calls to wait until a command is finished. Now, whenenever a command is sent to the debugger
#   the cmdlet Send-PowerDbgCommand waits until the command finish. Using this approach there won't happen
#   errors when you send a command that may take a long time to execute. At the same time the scripts using
#   PowerDbg don't need to implement delays anymore. It's a big performance improvement.
# - The script were revised and changed to be compatible with this new PowerDbg. To compatibilize I've
#   removed start-sleep and the regular "r" command (without arguments). Get the newest version.
# - Fixed bug that appeared when using WinDbg from the default path or when using a path that has
#   spaces, like: c:\Program Files
#
# For information about usage or to download PowerDbg scripts read: 
# http://blogs.msdn.com/debuggingtoolbox/archive/tags/PowerDbg+Library/default.aspx
#
# Roberto Alexis Farah
# Copyright (c) Microsoft Corporation.  All rights reserved. 
########################################################################################################

################################################################################ 
## 
## WinDbg.psm1 
## Automate WinDbg with PowerShell scripting 
## 
## To use: 
## In WinDbg, set up a server: .server tcp:Port=10456 
## In PowerShell:
#
## Import-Module windbg 
## Connect-Windbg "tcp:Port=10456,Server=<server>" 
##
## Other remote connection strings / protocols work, too. 
## Invoke-WindbgCommand k 
## Disconnect-Windbg 
##
## Author: 
##
## Modified by: Roberto A. Farah - http://blogs.msdn.com/debuggingtoolbox/
## 
## The modification redirects the output to the log file used by PowerDbg.
##
## Copyright (c) Microsoft Corporation.  All rights reserved. 
################################################################################ 

param($cdbPath = "C:\debuggers\cdb.exe") 

$SCRIPT:windbgProcess = $null 
$SCRIPT:currentConnection = $null 


## Connect to a windbg remote session 
function Connect-Dbg 
{ 
    param( 
        [Parameter(Mandatory = $true)] 
        $remote 
        ) 
        
    if($SCRIPT:currentConnection -and ($SCRIPT:currentConnection -ne $remote)) 
    { 
        throw "Already connected to $remote. Use Disconnect-Windbg, " +  
            "then connect to another instance." 
    } 
    
    ## Launch cdb.exe, the command-line version of WinDbg. 
    ## Take control of its input and output streams, which we'll use 
    ## to capture commands and their output. 
    if(-not $SCRIPT:currentConnection) 
    { 
        $processStartInfo = New-Object System.Diagnostics.ProcessStartInfo 
        $processStartInfo.FileName = $cdbPath 
        $processStartInfo.WorkingDirectory = (Get-Location).Path 
        $processStartInfo.Arguments = "-remote $remote" 
        $processStartInfo.UseShellExecute = $false 
        $processStartInfo.RedirectStandardInput = $true 
        $processStartInfo.RedirectStandardOutput = $true  

        $SCRIPT:windbgProcess =  
            [System.Diagnostics.Process]::Start($processStartInfo) 
        $SCRIPT:currentConnection = $remote 

        if(-not $SCRIPT:windbgProcess) 
        { 
            return 
        } 
        
        ## Ignore the stuff that was in the session before we 
        ## connected 
        $null = Receive-WindbgOutput 
    } 
} 


function Connect-WinDbg
{
    param( 
        [Parameter(Mandatory = $true)] 
        $remote 
        ) 
        
    Connect-Dbg $remote
}


## Disconnect from the session 
function Disconnect-Windbg 
{ 
    if($SCRIPT:windbgProcess -and (-not $SCRIPT:windbgProcess.HasExited)) 
    { 
        $SCRIPT:windbgProcess.StandardOutput.Close() 
        $SCRIPT:windbgProcess.StandardInput.Close() 
        $SCRIPT:windbgProcess.Kill() 
    } 
    
    $SCRIPT:currentConnection = $null 
    $SCRIPT:windbgProcess = $null 
} 

## Invoke a command in the connected WinDbg session, and return 
## its output 
function Invoke-WindbgCommand 
{ 
    if(-not $SCRIPT:windbgProcess) 
    { 
        throw "Not connected. Use Connect-Windbg to connect to an " + 
            "instance of WinDbg." 
    } 
       
    $SCRIPT:windbgProcess.StandardInput.WriteLine("$args") 
    
    Receive-WindbgOutput 
} 

## Retrieve pending output from the connected WinDbg session 
function Receive-WindbgOutput 
{        
    ## Add a special tag so that we know the end of the command 
    ## response 
    $sent = "PSWINDBG_COMPLETE_{0:o}" -f [DateTime]::Now  
    
    $SCRIPT:windbgProcess.StandardInput.WriteLine(".echo $sent") 
       
    $received = New-Object System.Text.StringBuilder 
    
    ## Wait for the response to end 
    while($received.ToString().IndexOf($sent) -lt 0) 
    { 
        $null = $SCRIPT:windbgProcess.StandardOutput.EndOfStream 
        
        while($SCRIPT:windbgProcess.StandardOutput.Peek() -ge 0) 
        { 
            $null = $received.Append($SCRIPT:windbgProcess.StandardOutput.Read(), 1) 
        } 
    } 
       
    [string] $global:g_commandOutput = $received.ToString() 
    
    # This approach is not good. I convert the string to an array and back to a string. Very time consuming.
    # Soon I should change this solution for a faster one.
    $temp = $global:g_commandOutput.Split("`r`n")
    
    # Remove the last two lines.
    $temp = $temp[0..($temp.Count - 3)]
    
    $global:g_commandOutput = ""  # Resets string.
    
    for($i = 0; $i -lt $temp.Count; $i++)
    {
        $global:g_commandOutput += $temp[$i] + "`r`n"  # Recreates the string without the last two lines.
    }

}


$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { Disconnect-Windbg } 


# Global variables.
########################################################################################################
$global:g_instance               = $null
$global:g_fileParsedOutput       = "POWERDBG-PARSED.LOG"
[string] $global:g_commandOutput = ""
$global:g_CSVDelimiter           = ","
$global:g_frameDelimiter         = "#$#@"

# It's not possible to create an enum like C++/C# using a PowerShell keyword.
# The PowerShell blog has a solution for it, but here I'm going to use local variables.
$global:g_unknownSymbol             = 0
$global:g_waitingForCriticalSection = 1
$global:g_doingIO                   = 2
$global:g_threadWaiting             = 3
$global:g_GCthread                  = 4
$global:g_waitUntilGCComplete       = 5
$global:g_suspendForGC              = 6
$global:g_waitForFinalize           = 7
$global:g_tryingGetLock             = 8
$global:g_winSockReceivingData      = 9
$global:g_finalizerThread           = 10
$global:g_CLRDebuggerThread         = 11
$global:g_w3wpMainThread            = 12
$global:g_threadPool                = 13
$global:g_CompressionThreaad        = 14
$global:g_COMcall                   = 15
$global:g_CLRWorkerThread           = 16
$global:g_completionPortIOThread    = 17
$global:g_gateThread                = 18
$global:g_timerThread               = 19
$global:g_unloadAppDomainThread     = 20
$global:g_RPCWorkerThread           = 21
$global:g_LPRCWorkerThread          = 22
$global:g_hostSTAThread             = 23

# The array below has the meaning of each constant above. It can be used to display high level information
# to the user.
$global:g_whatThreadIsDoing = 
@(
    "Thread working and doing unknown activity.",                                                   # 0
    "Thread waiting for Critical Section.",                                                         # 1
    "Thread doing I/O operation.",                                                                  # 2
    "Thread in wait state.",                                                                        # 3
    "Thread from Garbage Collector.",                                                               # 4
    "Thread waiting for the Garbage Collector to complete.",                                        # 5
    "Thread is being suspended to perform a Garbage Collector."                                     # 6
    "Thread waiting for the Finalizer event. The Finalizer thread might be blocked.",               # 7
    "Thread trying to get a managed lock.",                                                         # 8
    "Thread receiving or trying to receive data. The data might be from the database.",             # 9
    "Thread is the Finalizer Thread.",                                                              # 10
    "Thread is the CLR Debugger Thread.",                                                           # 11
    "Thread is the W3WP.EXE main thread.",                                                          # 12
    "Thread is from the W3WP.EXE pool of threads.",                                                 # 13
    "Thread is a Compression Thread from W3WP.EXE.",                                                # 14
    "Thread doing a COM call.",                                                                     # 15
    "Thread is a CLR Worker Thread.",                                                               # 16
    "Thread is a Completion Port I/O Thread.",                                                      # 17
    "Thread is a CLR Gate Thread.",                                                                 # 18
    "Thread is a CLR Timer Thread.",                                                                # 19
    "Thread is an Unload Application Domain Thread.",                                               # 20
    "Thread is a RPC Worker Thread.",                                                               # 21
    "Thread is an LRPC Worker Thread.",                                                             # 22
    "Thread is the Host STA Thread."                                                                # 23
)


########################################################################################################
# Function:    Send-PowerDbgCommand
#
# Parameters:  [string] <$command> 
#              Command from Windbg. Avoid mixing more than one command at the same line to be easier to parse the output.
#
# Return:      Nothing.
#
# Purpose:     Sends a command to the Windbg instance that has the PowerDbg title and saves the command and its output 
#              into a log file named POWERDBG.LOG.
#              The command output will be into the POWERDBG-OUTPUT.LOG.
#              Your parser functions should use POWERDBG-OUTPUT.LOG.
#
# Changes History: 12/31/2007 - In a few specific scenarios the .logopen command may not create the log file. 
#                               Now the POWERDBG.LOG is tested. If it was not created, the function tries to
#                               create it for a limited number of times.
#
#                  03/28/2008 - No more delays while waiting for a command to be executed. Now whenever a
#                               command is sent to the debugger, the debugger sends a message and close
#                               the log file when it's done. The cmdlet verifies the log to see if the command
#                               finished its execution. If yes it returns, otherwise it keeps waiting.
#
#                  04/06/2008 - .logopen was being used without ", so failing when the Debugger was installed
#                               in a folder that has spaces, like: : c:\Program Files.
#
#                  06/30/2008 - Default timeout is now 1 hour. Fixed small bug when trying to identify
#                               #### IGNORE REGISTERS ABOVE ####. Sometimes the initial or final char may be
#                               replaced, causing an infinite loop.
#                  
#                  07/03/2008 - Sometimes the debugger replace lower case by upper case. When executing an extension
#                               we may get the message "No export <commandname> found". Thus, if PowerDbg identifies it
#                               the command is sent again.
#
#                  01/21/2009 - Send-PowerDbgCommand uses Lee Holmes' approach to send commands to WinDbg.
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Send-PowerDbgCommand([string] $command = $(throw "Error! You must provide a Windbg command."))
{
    set-psdebug -strict      
    $ErrorActionPreference = "stop"

    Invoke-WinDbgCommand "$command"
}




########################################################################################################
# Function:    Parse-PowerDbgDT
#
# Parameters:  [switch] [$useFieldNames] 
#              Switch flag. If $useFieldNames is present then the function saves the field
#              names from struct/classes and their values. Otherwise, it saves the offsets
#              and their values.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "dt" command using a hash table. The output 
#              is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
#              Usage: 
#              Parse-PowerDbgDT
#              Parse-PowerDbgDT -useFieldNames
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgDT([switch] $useFieldNames)
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # \s+   --> Scans for one or more spaces.
    # (\S+) --> Gets one or more chars/digits/numbers without spaces.
    # \s+   --> Scans for one or more spaces.
    # (\w+) --> Gets one or more chars.
    # .+    --> Scans for one or more chars (any char except for new line).
    # \:    --> Scans for the ':' char.
    # \s+   --> Scans for one or more spaces.
    # (.+.) --> Gets the entire remainder string including the spaces.
    if($useFieldNames)
    {
        # This is to be able to read each line.
        $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
        # Scan the symbols for each thread, line by line.
        while(($line = $stringReader.ReadLine()) -ne $null)
        { 
           if($line -match "0x\S+\s+(?<key>\w+).+\:\s+(?<value>.+)")
           {
               $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
           } 
        }         
    }
    else
    {
        # This is to be able to read each line.
        $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
        # Scan the symbols for each thread, line by line.
        while(($line = $stringReader.ReadLine()) -ne $null)
        { 
           if($line -match "(?<key>0x\S+).+\:\s+(?<value>.+)")
           {
               $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
           } 
        }  
    }
    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}


########################################################################################################
# Function:    Convert-PowerDbgCSVtoHashTable
#
# Parameters:  None.
#                                         
# Return:      Hash table.
#
# Purpose:     Sometimes the Parse-PowerDbg#() functions return a CSV file. This function
#              loads the data using a hash table. 
#              However, it works just when the CSV file has two fields: key and value.
#
# Changes History: 04/03/2009 The cmdlet was changed to ignore the new line between the header from the CSV file and data.
#                  So now it ignores the new line that counted as a hash table item.
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Convert-PowerDbgCSVtoHashTable()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    $hashTable = @{}
    import-csv -path $global:g_fileParsedOutput | foreach {if($_.key -ne ""){$hashTable[$_.key] = $_.value}}

    return $hashTable
}



########################################################################################################
# Function:    Send-PowerDbgDML
#
# Parameters:  [string] <$hyperlinkDML>
#              Hyperlink for the DML command.
#
#              [string] <$commandDML>
#              Command to execute when the hyperlink is clicked.
#                                         
# Return:      Nothing.
#
# Purpose:     Creates a DML command and sends it to Windbg. 
#              DML stands for Debug Markup Language. Using DML you can create hyperlinks that
#              run a command when the user click on them.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Send-PowerDbgDML(
                          [string] $hyperlinkDML = $(throw "Error! You must provide the hyperlink for DML."),
                          [string] $commandDML   = $(throw "Error! You must provide the command for DML.")
                         )
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    Send-PowerDbgCommand ".printf /D `"<link cmd=\`"$commandDML\`"><b>$hyperlinkDML</b></link>\n\`"`""
}


########################################################################################################
# Function:    Parse-PowerDbgNAME2EE
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!name2ee" command using a hash table. The output 
#              is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgNAME2EE()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # \s+   --> Scans for one or more spaces.
    # (\S+) --> Gets one or more chars/digits/numbers without spaces.
    # \s+   --> Scans for one or more spaces.
    # (\w+) --> Gets one or more chars.
    # .+    --> Scans for one or more chars (any char except for new line).
    # \:    --> Scans for the ':' char.
    # \s+   --> Scans for one or more spaces.
    # (.+.) --> Gets the entire remainder string including the spaces.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
       # Attention! The Name: doesn't map to the right value, however, it should be the same method name provide as argument.
       if($line -match "(?<key>\w+\:)\s+(?<value>\S+)")
       {
           $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
       } 
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}


########################################################################################################
# Function:    Parse-PowerDbgDUMPMD
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!dumpmd" command using a hash table. The output 
#              is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgDUMPMD()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # \s+   --> Scans for one or more spaces.
    # (\S+) --> Gets one or more chars/digits/numbers without spaces.
    # \s+   --> Scans for one or more spaces.
    # (\w+) --> Gets one or more chars.
    # .+    --> Scans for one or more chars (any char except for new line).
    # \:    --> Scans for the ':' char.
    # \s+   --> Scans for one or more spaces.
    # (.+.) --> Gets the entire remainder string including the spaces.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
       if($line -match "(?<key>((^Method Name :)|(^MethodTable)|(^Module:)|(^mdToken:)|(^Flags :)|(^Method VA :)))\s+(?<value>\S+)")
       {
           $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
       } 
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}


########################################################################################################
# Function:    Parse-PowerDbgDUMPMODULE
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!dumpmodule" command using a hash table. The output 
#              is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgDUMPMODULE()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    [int] $countFields = 0
    
    # \s+   --> Scans for one or more spaces.
    # (\S+) --> Gets one or more chars/digits/numbers without spaces.
    # \s+   --> Scans for one or more spaces.
    # (\w+) --> Gets one or more chars.
    # .+    --> Scans for one or more chars (any char except for new line).
    # \:    --> Scans for the ':' char.
    # \s+   --> Scans for one or more spaces.
    # (.+.) --> Gets the entire remainder string including the spaces.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
       # Fields for .NET Framework 2.0
       if($line -match "(?<key>((^dwFlags)|(^Assembly:)|(^LoaderHeap:)|(^TypeDefToMethodTableMap:)|(^TypeRefToMethodTableMap:)|(^MethodDefToDescMap:)|(^FieldDefToDescMap:)|(^MemberRefToDescMap:)|(^FileReferencesMap:)|(^AssemblyReferencesMap:)|(^MetaData start address:)))\s+(?<value>\S+)")
       {
           $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
           $countFields++
       } 
    }  

    # If nothing was found, let's try to use the .NET Framework 1.1 fields.
    if($countFields -lt 3)
    {
        # This is to be able to read each line.
        $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
        # Scan the symbols for each thread, line by line.
        while(($line = $stringReader.ReadLine()) -ne $null)
        { 
           # Fields for .NET Framework 2.0
           if($line -match "(?<key>((^dwFlags)|(^Assembly\*)|(^LoaderHeap\*)|(^TypeDefToMethodTableMap\*)|(^TypeRefToMethodTableMap\*)|(^MethodDefToDescMap\*)|(^FieldDefToDescMap\*)|(^MemberRefToDescMap\*)|(^FileReferencesMap\*)|(^AssemblyReferencesMap\*)|(^MetaData starts at)))\s+(?<value>\S+)")
           {
               $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
               $hasFound = $true
           } 
        }  
    
    }
    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}

########################################################################################################
# Function:    Parse-PowerDbgLMI
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "lmi" command using a hash table. The output 
#              is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgLMI()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # \s+   --> Scans for one or more spaces.
    # (\S+) --> Gets one or more chars/digits/numbers without spaces.
    # \s+   --> Scans for one or more spaces.
    # (\w+) --> Gets one or more chars.
    # .+    --> Scans for one or more chars (any char except for new line).
    # \:    --> Scans for the ':' char.
    # \s+   --> Scans for one or more spaces.
    # (.+.) --> Gets the entire remainder string including the spaces.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
       if($line -match "(?<key>((^.+\:)))\s+(?<value>\S+)")
       {     
           $strNoLeftSpaces = $matches["key"]
           $strNoLeftSpaces = $strNoLeftSpaces.TrimStart()
           $builder = $builder.AppendLine($strNoLeftSpaces + $global:g_CSVDelimiter + $matches["value"])    
       } 
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}


########################################################################################################
# Function:    Has-PowerDbgCommandSucceeded
#
# Parameters:  None.
#                                         
# Return:      Return $true if the last command succeeded or $false if not.
#
# Purpose:     Return $true if the last command succeeded or $false if not.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Has-PowerDbgCommandSucceeded
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # This is to be able to read each line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
       if($line -imatch "(Fail) | (Failed) | (Error) | (Invalid) | (Unable to get) | (Exception)")
       {     
           return $false   
       } 
    }  
    return $true
}


########################################################################################################
# Function:    Send-PowerDbgComment
#
# Parameters:  [string] $comment
#              Comment to be sent to the debugger.
#                                         
# Return:      Nothing.
#
# Purpose:     Sends a bold comment to the debugger. Uses DML.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Send-PowerDbgComment(
                                 [string] $comment = $(throw "Error! You must provide a comment.")
                             )
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    Send-PowerDbgCommand ".printf /D `"\n<b>$comment</b>\n\n\`"`""
}


########################################################################################################
# Function:    Parse-PowerDbgVERTARGET
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output of Kernel time and User time from "vertarget" command, using a hash table. The output 
#              is saved into the file POWERDBG-PARSED.LOG 
#              The number of days is ignored in this version.
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgVERTARGET()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # \s+   --> Scans for one or more spaces.
    # (\S+) --> Gets one or more chars/digits/numbers without spaces.
    # \s+   --> Scans for one or more spaces.
    # (\w+) --> Gets one or more chars.
    # .+    --> Scans for one or more chars (any char except for new line).
    # \:    --> Scans for the ':' char.
    # \s+   --> Scans for one or more spaces.
    # (.+.) --> Gets the entire remainder string including the spaces.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
       if($line -match "(?<key>((Kernel time:)|(User time:)))\s+\d+\s+\S+\s+(?<value>\d+\:\d+\:\d+\.\d+)")   
       {
           $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
       } 
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}


########################################################################################################
# Function:    Parse-PowerDbgRUNAWAY
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output of "!runaway 1" or "!runaway 2" command, using a hash table. 
#              For this version the number of days is not being considered.
#              The output is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
#              Attention! If you need to know the top threads consuming CPU time use the Convert-PowerDbgRUNAWAYtoArray
#              instead of this command. With Convert-PowerDbgRUNAWAYtoArray, the array has the exact same order of the
#              original command.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgRUNAWAY()
{   
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # \s+   --> Scans for one or more spaces.
    # (\S+) --> Gets one or more chars/digits/numbers without spaces.
    # \s+   --> Scans for one or more spaces.
    # (\w+) --> Gets one or more chars.
    # .+    --> Scans for one or more chars (any char except for new line).
    # \:    --> Scans for the ':' char.
    # \s+   --> Scans for one or more spaces.
    # (.+.) --> Gets the entire remainder string including the spaces.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
        if($line -match "(?<key>(\d+))\:\S+\s+\d+\s+\S+\s+(?<value>\d+\:\d+\:\d+\.\d+)")   
        {
            $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
        } 
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}



########################################################################################################
# Function:    Convert-PowerDbgRUNAWAYtoArray
#
# Parameters:  None.
#                                         
# Return:      Two dimensions array.
#
# Purpose:     After executing the !runaway 1 or !runaway 2, use this command to put the information into
#              an array.
#              
# Changes History: 04/03/09 Changes to accomodate PowerDbg v5.2 that doesn't use log files anymore.
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Convert-PowerDbgRUNAWAYtoArray()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}
   
    # Now, we create a multidimensional array.
    # We need to discard 3 lines that corresponds to:
    # User Mode Time
    #  Thread       Time
    $arrayFromRUNAWAY = new-Object 'object[,]' (($global:g_commandOutput.Split("`r`n").count - 5) / 2),2
    
    [System.Int32] $i = 0   # Counter.
    
    # This is to be able to read each line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
        if($line -match  "(?<key>(\d+))\:\S+\s+\d+\s+\S+\s+(?<value>\d+\:\d+\:\d+\.\d+)")   
        {
            $arrayFromRUNAWAY[$i, 0] = $matches["key"]
            
            $arrayFromRUNAWAY[$i, 1] = $matches["value"]
        
            $i++
        } 
    }  
    
    # The cmoma below is very important, otherwise the function will return a single dimension array.
    return ,$arrayFromRUNAWAY
}



########################################################################################################
# Function:    Parse-PowerDbgK
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output of "k" command and variations like "kv, kbn, kpn", etc..., using a hash table. 
#              It doesn't work with "kPn".
#              The key is the thread number if you use something like "~* kbn" or the key is "#" if you use 
#              something like "kb" just to show the stack from the current thread.
#              Frame are separated by '$global:g_frameDelimiter'. So, to display the frames using newline you need to
#              replace before displaying.
#
#              The output is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
#              Attention! 
#              1- It doesn't work with "kPn".
#              2- It replaces "," by '$global:g_frameDelimiter' to avoid conflict with CSV delimiter.
#
# Changes History: 12/21/2007 - The number of threads couldn't exceed 2 digits. Now it works until 999 threads.
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgK()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    $key = ""
    
    # [ #]  --> One space or #
    # \s    --> Just one space.
    # (d+)  --> Returns the decimal digits.
    # \s+   --> 1 or more spaces.
    # |     --> or
    # \s    --> One space.
    # (#)   --> Returns #.
    # \s    --> One space.
    # |     --> or
    # (\w+\s\w+\s.+) --> Returns two blocks of words and the remaining string.
    # |     --> or
    # (\d+\s\w+\s\w+\s.+) --> Returns one block of digits + two blocks of words + remaining string.
    # [ #]\s(\d+)\s+|\s(#)\s|(\w+\s\w+\s.+)|(\d+\s\w+\s\w+\s.+)  <-- The actual implementation differs a little bit.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    {     
       # For each call stack we have the key that is the thread number or # and the value that are a set
       # of lines, including all frames, etc...
       # Here we first identify the thread number if it was not identified before.
       if($line -match "([ #]\s*(?<key>(\d+))\s+Id)") 
       {              
           # If key changed append a new line. Do not consider one single thread, that is represented by "#"
           if($key -ne $matches["key"])
           {
               # The string assignment is a small tricky to avoid displaying contents in PS window.
               $builder = $builder.AppendLine("")
               $key = $matches["key"]           
           }
        
           # Just add the key. The stack is the value and it's going to be added below.
           # The string assignment is a small tricky to avoid displaying contents in PS window.        
           $builder = $builder.Append($matches["key"] + $global:g_CSVDelimiter)        
       } 
       elseif($line -match "(?<value>(\w+\s\w+\s.+))")  # Gets the stack.    
       {
           # If there is just one thread the thread number doesn't appear. For this case the thread number
           # will be "#".
           if($key -eq "")
           {
               $key = "#"
            
               # Just add the key. The stack is the value and it's going to be added below.
               # The string assignment is a small tricky to avoid displaying contents in PS window.        
               $builder = $builder.Append($key + $global:g_CSVDelimiter)                                   
           }
        
           # Append each frame replacing any commas by ";".
           # The value part of the hash table is a long string with all frames. At the end of each frame there is 
           # a delimiter. When you want to show the stack you know you can replace the delimiter by `r`n.
           # Using a delimiter is easy to do that.
           $builder = $builder.Append($matches["value"].Replace(",",";") + $global:g_frameDelimiter)              
       }
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}




########################################################################################################
# Function:    Parse-PowerDbgSymbolsFromK
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps just the symbols of "k" command and variations like "kv, kbn, kpn", etc..., using a hash table. 
#              It doesn't work with "kPn".
#              The key is the thread number if you use something like "~* kbn" or the key is "#" if you use 
#              something like "kb" just to show the stack from the current thread.
#              Frame are separated by '$global:g_frameDelimiter'. So, to display the frames using newline you need to
#              replace before displaying.
#
#              The output is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
#              Attention! 
#              1- It doesn't work with "kPn".
#              2- It replaces "," by '$global:g_frameDelimiter' to avoid conflict with CSV delimiter.
#
# Changes History: 12/21/2007 - The number of threads couldn't exceed 2 digits. Now it works until 999 threads.
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgSymbolsFromK()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    $key = ""
    
    # [ #]  --> One space or #
    # \s    --> Just one space.
    # (d+)  --> Returns the decimal digits.
    # \s+   --> 1 or more spaces.
    # |     --> or
    # \s    --> One space.
    # (#)   --> Returns #.
    # \s    --> One space.
    # |     --> or
    # (\w+\s\w+\s.+) --> Returns two blocks of words and the remaining string.
    # |     --> or
    # (\d+\s\w+\s\w+\s.+) --> Returns one block of digits + two blocks of words + remaining string.
    # [ #]\s(\d+)\s+|\s(#)\s|(\w+\s\w+\s.+)|(\d+\s\w+\s\w+\s.+)  <-- The actual implementation differs a little bit.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    {     
       # For each call stack we have the key that is the thread number or # and the value that are a set
       # of lines, including all frames, etc...
       # Here we first identify the thread number if it was not identified before.
       if($line -match "([ #]\s*(?<key>(\d+))\s+Id)")  
       {              
           # If key changed append a new line. Do not consider one single thread, that is represented by "#"
           if($key -ne $matches["key"])
           {
               # The string assignment is a small tricky to avoid displaying contents in PS window.
               $builder = $builder.AppendLine("")
               $key = $matches["key"]
           }
        
           # Just add the key. The stack is the value and it's going to be added below.
           # The string assignment is a small tricky to avoid displaying contents in PS window.        
           $builder = $builder.Append($matches["key"] + $global:g_CSVDelimiter)        
       } 
       elseif($line -match "\s(?<value>(\w+!\w+::\w+))|(?<value>(\w+!\w+)|(?<value>(\w+_ni)))")  # Gets the symbols from the stack.
       {
           # \s(\w+!\w+::\w+)|(\w+!\w+)
           # (\w+!\w+::\w+) <-- Find possible C++ methods.
           # (\w+!\w+)      <-- Find regular symbols.
           # The order is important here because the "or" is not going to evaluate the second expression if the
           # first expression returns true. 
        
           # If there is just one thread the thread number doesn't appear. For this case the thread number
           # will be "#".
           if($key -eq "")
           {
               $key = "#"
            
               # Just add the key. The stack is the value and it's going to be added below.
               # The string assignment is a small tricky to avoid displaying contents in PS window.        
               $builder = $builder.Append($key + $global:g_CSVDelimiter)                                   
           }
        
           # Append each symbol frame found plus the delimiter.
           $builder = $builder.Append($matches["value"] + $global:g_frameDelimiter)              
       }
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}


########################################################################################################
# Function:    Parse-PowerDbgLM1M
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output of "lm1m".
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgLM1M()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # This is to be able to read each line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
        if($line -match "(?<key>(\w+))")   
        {
            # Value and key has the same value for this particular parser.
            $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["key"])    
        } 
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}



########################################################################################################
# Function:    Classify-PowerDbgThreads
#
# Parameters:  None.
#                                         
# Return:      Array where the index is the thread number and the element is one of these values:
#              0 UNKNOWN_SYMBOL
#              1 WAITING_FOR_CRITICAL_SECTION
#              2 DOING_IO
#              3 WAITING
#              4 GC_THREAD
#              5 WAIT_UNTIL_GC_COMPLETE
#              6 SUSPEND_FOR_GC
#              7 WAIT_FOR_FINALIZE
#              8 TRYING_MANAGED_LOCK
#              9 DATA_FROM_WINSOCK
#              
#              The constants above are stored in global variables. 
#
# Purpose:     Returns an array which the index corresponds to thread numbers and the content is a value represented
#              by the constants above. This cmdlet gives you an idea of what the threads are doing.
#              Notice that is very easy to add more symbols and more constants to get a more granular analysis.
#
# Changes History: 01/25/08 - More symbols added to the hash table. The analysis became more granular.
#
# Mike McIntyre
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Classify-PowerDbgThreads()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}
    
    $symbolsForEachThread = @{}
    
    # Let's save resource. We need kL with just 8 frames.
    Send-PowerDbgCommand "~* kL 8"
    
    $hasCommandSucceeded = Has-PowerDbgCommandSucceeded

    # Check if the command was executed with success.
    # It's unlikely to have a failure with the "k" command, but I'm being proactive.
    if($false -eq $hasCommandSucceeded)
    {
        throw "Couldn't get call stacks!"
    }
    
    # Let's parse the output of the "k" command and variations, however, we want the symbols
    # not the complete call stack.
    Parse-PowerDbgSymbolsFromK
    
    # Now let's get the symbols for each thread. We don't need the stack details here.
    $symbolsForEachThread = Convert-PowerDbgCSVtoHashTable
        
    # IMPORTANT!!!
    # The symbols need to be written in uppercase, because the comparison is doing using uppercase letters to avoid
    # mismatches.
    $symbols = @{ 
                  "KERNEL32!COPYFILE"                                 = $global:g_doingIO;
                  "KERNEL32!CREATEDIRECTORY"                          = $global:g_doingIO;
                  "KERNEL32!CREATEFILE"                               = $global:g_doingIO;  
                  "KERNEL32!DELETEFILE"                               = $global:g_doingIO;
                  "KERNEL32!FILEIOCOMPLETIONROUTINE"                  = $global:g_doingIO; 
                  "KERNEL32!FINDCLOSE"                                = $global:g_doingIO;      
                  "KERNEL32!FINDCLOSECHANGENOTIFICATION"              = $global:g_doingIO;
                  "KERNEL32!FINDFIRSTCHANGENOTIFICATION"              = $global:g_doingIO;
                  "KERNEL32!FINDFIRSTFILE"                            = $global:g_doingIO;          
                  "KERNEL32!FINDFIRSTFILEEX"                          = $global:g_doingIO;              
                  "KERNEL32!FINDNEXTCHANGENOTIFICATION"               = $global:g_doingIO;
                  "KERNEL32!FINDNEXTFILE"                             = $global:g_doingIO;              
                  "KERNEL32!FLUSHFILEBUFFERS"                         = $global:g_doingIO;
                  "KERNEL32!GETBINARYTYPE"                            = $global:g_doingIO;                 
                  "KERNEL32!GETCURRENTDIRECTORY"                      = $global:g_doingIO;
                  "KERNEL32!GETDRIVETYPE"                             = $global:g_doingIO;                 
                  "KERNEL32!GETFILEATTRIBUTES"                        = $global:g_doingIO;                     
                  "KERNEL32!GETFILEATTRIBUTESEX"                      = $global:g_doingIO;                     
                  "KERNEL32!GETFILEINFORMATIONBYHANDLE"               = $global:g_doingIO;                     
                  "KERNEL32!GETFILESIZE"                              = $global:g_doingIO;                     
                  "KERNEL32!GETFILESIZEEX"                            = $global:g_doingIO;                     
                  "KERNEL32!GETFULLPATHNAME"                          = $global:g_doingIO;                         
                  "KERNEL32!GETTEMPFILENAME"                          = $global:g_doingIO;                          
                  "KERNEL32!GETTEMPPATH"                              = $global:g_doingIO;                          
                  "KERNEL32!LOCKFILE"                                 = $global:g_doingIO;                          
                  "KERNEL32!LOCKFILEEX"                               = $global:g_doingIO;                                            
                  "KERNEL32!MOVEFILE"                                 = $global:g_doingIO;                                            
                  "KERNEL32!READDIRECTORYCHANGESW"                    = $global:g_doingIO;                                            
                  "KERNEL32!READFILE"                                 = $global:g_doingIO;                                            
                  "KERNEL32!READFILEEX"                               = $global:g_doingIO;                                            
                  "KERNEL32!REMOVEDIRECTORY"                          = $global:g_doingIO;
                  "KERNEL32!SEARCHPATH"                               = $global:g_doingIO;    
                  "KERNEL32!SETCURRENTDIRECTORY"                      = $global:g_doingIO;   
                  "KERNEL32!SETENDOFFILE"                             = $global:g_doingIO;          
                  "KERNEL32!SETFILEATTRIBUTES"                        = $global:g_doingIO; 
                  "KERNEL32!SETFILEPOINTER"                           = $global:g_doingIO;  
                  "KERNEL32!SETFILEPOINTEREX"                         = $global:g_doingIO;                  
                  "KERNEL32!UNLOCKFILE"                               = $global:g_doingIO;                      
                  "KERNEL32!UNLOCKFILEEX"                             = $global:g_doingIO;                      
                  "KERNEL32!WRITEFILE"                                = $global:g_doingIO;                          
                  "KERNEL32!WRITEFILEEX"                              = $global:g_doingIO;
                  "NTDLL!ZWREMOVEIOCOMPLETION"                        = $global:g_doingIO;
                  "NTDLL!RTLPWAITFORCRITICALSECTION"                  = $global:g_waitingForCriticalSection;  
                  "NTDLL!RTLENTERCRITICALSECTION"                     = $global:g_waitingForCriticalSection;  
                  "KERNEL32!ENTERCRITICALSECTION"                     = $global:g_waitingForCriticalSection;  
                  "KERNEL32!MSGWAITFORMULTIPLEOBJECTS"                = $global:g_threadWaiting;
                  "KERNEL32!MSGWAITFORMULTIPLEOBJECTSEX"              = $global:g_threadWaiting;
                  "KERNEL32!REGISTERWAITFORSINGLEOBJECT"              = $global:g_threadWaiting;
                  "KERNEL32!SIGNALOBJECTANDWAIT"                      = $global:g_threadWaiting;
                  "KERNEL32!UNREGISTERWAIT"                           = $global:g_threadWaiting;
                  "KERNEL32!UNREGISTERWAITEX"                         = $global:g_threadWaiting; 
                  "KERNEL32!WAITFORMULTIPLEOBJECTS"                   = $global:g_threadWaiting;
                  "KERNEL32!WAITFORMULTIPLEOBJECTSEX"                 = $global:g_threadWaiting; 
                  "KERNEL32!WAITFORSINGLEOBJECT"                      = $global:g_threadWaiting; 
                  "KERNEL32!WAITFORSINGLEOBJECTEX"                    = $global:g_threadWaiting; 
                  "KERNEL32!WAITORTIMERCALLBACK"                      = $global:g_threadWaiting;
                  "USER32!NTUSERGETMESSAGE"                           = $global:g_threadWaiting;
                  "USER32!NTUSERMESSAGECALL"                          = $global:g_threadWaiting;
                  "USER32!NTUSERWAITMESSAGE"                          = $global:g_threadWaiting;
                  "NTDLL!DBGBREAKPOINT"                               = $global:g_threadWaiting; 
                  "NTDLL!RTLPWAITTHREAD"			                  = $global:g_threadWaiting; 
                  "KERNEL32!SLEEPEX"                                  = $global:g_threadWaiting;
                  "KERNEL32!SLEEP"                                    = $global:g_threadWaiting;
                  "NTDLL!NTDELAYEXECUTION"                            = $global:g_threadWaiting;
                  "MFC80D!AFXINTERNALPUMPMESSAGE"                     = $global:g_threadWaiting;
                  "MFC80!AFXINTERNALPUMPMESSAGE"                      = $global:g_threadWaiting;
                  "MSCORWKS!SVR::GC_HEAP::GC_THREAD_STUB"             = $global:g_GCthread;
                  "MSCORSVR!SVR::GC_HEAP::GC_THREAD_STUB"             = $global:g_GCthread;
                  "MSCORSVR!GCHEAP::WAITUNTILGCCOMPLETE"              = $global:g_waitUntilGCComplete;
                  "MSCORWKS!GCHEAP::WAITUNTILGCCOMPLETE"              = $global:g_waitUntilGCComplete;
                  "MSCORWKS!THREAD::SYSSUSPENDFORGC"                  = $global:g_suspendForGC;
                  "MSCORSVR!THREAD::SYSSUSPENDFORGC"                  = $global:g_suspendForGC;
                  "MSCORWKS!WAITFORFINALIZEREVENT"                    = $global:g_waitForFinalize;
                  "MSCORSVR!WAITFORFINALIZEREVENT"                    = $global:g_waitForFinalize;
                  "MSCORWKS!SVR::WAITFORFINALIZEREVENT"               = $global:g_waitForFinalize;                
                  "MSCORSVR!SVR::WAITFORFINALIZEREVENT"               = $global:g_waitForFinalize;                
                  "MSCORWKS!WKS::WAITFORFINALIZEREVENT"               = $global:g_waitForFinalize; 
                  "MSCORSVR!WKS::WAITFORFINALIZEREVENT"               = $global:g_waitForFinalize; 
                  "MSCORWKS!JITUNTIL_MONCONTENTION"                   = $global:g_tryingGetLock;
                  "MSCORSVR!JITUNTIL_MONCONTENTION"                   = $global:g_tryingGetLock;
                  "MSWSOCK!WSPRECV"                                   = $global:g_winSockReceivingData;
                  "WS2_32!RECV"                                       = $global:g_winSockReceivingData;
                  "MSCORSVR!GCHeap::FINALIZETHREADSTART"              = $global:g_finalizerThread;
                  "MSCORWKS!GCHeap::FINALIZETHREADSTART"              = $global:g_finalizerThread;
                  "MSCORSVR!DEBUGGERRCTHREAD::MAINLOOP"               = $global:g_CLRDebuggerThread;
                  "MSCORWKS!DEBUGGERRCTHREAD::MAINLOOP"               = $global:g_CLRDebuggerThread;
                  "W3DT!WP_CONTEXT::RUNMAINTHREADLOOP"                = $global:g_w3wpMainThread;
                  "W3TP!THREAD_POOL_DATA::THREADPOOLTHREAD"           = $global:g_threadPool;
                  "W3CORE!HTTP_COMPRESSION::COMPRESSIONTHREAD"        = $global:g_CompressionThreaad;
                  "OLE32!CRPCCHANNELBUFFER::SENDRECEIVE2"             = $global:g_COMcall;
                  "MSCORWKS!CLREVENT::WAIT"                           = $global:g_CLRWorkerThread;
                  "MSCORSVR!CLREVENT::WAIT"                           = $global:g_CLRWorkerThread;
                  "MSCORWKS!THREADPOOLMGR::COMPLETIONPORTTHREADSTART" = $global:g_completionPortIOThread;
                  "MSCORSVR!THREADPOOLMGR::COMPLETIONPORTTHREADSTART" = $global:g_completionPortIOThread;
                  "MSCORWKS!THREADPOOLMGR::GATETHREADSTART"           = $global:g_gateThread;
                  "MSCORSVR!THREADPOOLMGR::GATETHREADSTART"           = $global:g_gateThread;
                  "MSCORWKS!THREADPOOLMGR::TIMERTHREADSTART"          = $global:g_timerThread;
                  "MSCORSVR!THREADPOOLMGR::TIMERTHREADSTART"          = $global:g_timerThread;                
                  "MSCORWKS!APPDOMAIN::ADUNLOADTHREADSTART"           = $global:g_unloadAppDomainThread;
                  "MSCORSVR!APPDOMAIN::ADUNLOADTHREADSTART"           = $global:g_unloadAppDomainThread;
                  "RPCRT4!THREADSTARTROUTINE"                         = $global:g_RPCWorkerThread;
                  "RPCRT4!COMMON_PROCESSCALLS"                        = $global:g_RPCWorkerThread;
                  "RPCRT4!LRPC_ADDRESS::RECEIVELOTSACALLS"            = $global:g_LPRCWorkerThread;
                  "OLE32!CDLLHOST::STAWORKERLOOP"                     = $global:g_hostSTAThread
                }
    
    # The array has the right size to fit all threads.
    $array = 1..$symbolsForEachThread.count
        
    # Now we scan all threads and for each threads we scan all frames until symbols mapped to the hash table are found
    # or there are no more frames.
    # Below, we need to discount the hash table "key" and "value" strings because they are considered one element.
    for([System.Int32] $i = 0; $i -lt $symbolsForEachThread.count; $i++)
    {              
        # The delimiter is converted to new line. Now it's easy to process each line.
        $stack = $symbolsForEachThread[$i.ToString()].Replace($global:g_FrameDelimiter, "`n")
        
        # Sets the default value.
        $array[$i] = $global:g_unknownSymbol
        
        # This is to be able to read each line.
        $stringReader = [System.IO.StringReader] $stack
        
        # Scan the symbols for each thread, line by line.
        while(($frame = $stringReader.ReadLine()) -ne $null)
        {                                  
            # Now we try to locate the symbol in our hash table.
            # Always using uppercase.
            if($symbols[$frame.ToUpper()] -ne $null)
            {
                # If symbol not located we don't assign $null to the array.
                if($symbols[$frame.ToUpper()] -ne $null)
                {
                    # If found we assign the constant to the array element.
                    $array[$i] = $symbols[$frame.ToUpper()]
                }
            }
        }
    }
    
    # Force resources to be freed from memory.                         
    $symbols              = $null                 
    $symbolsForEachThread = $null
    
    return $array
}


########################################################################################################
# Function:    Analyze-PowerDbgThreads
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Analyzes and displays what each thread is doing and the CPU time, sorted by User time.
#              This cmdlet is very useful for hangs, high CPU and crashes scenarios.
#
#              Attention! If you have a mini-dump with no thread information you may want to create
#              a simplified cmdlet that doesn't use the information from !runaway.
#              To do that just remove all parts of this script that use User and Kernel time. :)
#              This script tries to use the CPU time because it's unlikely to have a dump that not have it.
#
# Changes History: 01/25/08 - Threads with unknown symbol appear in red color.
#                  04/03/09 - Small changes for version 5.2. 
#
# Mike McIntyre
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Analyze-PowerDbgThreads()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}
    
    # Let's get User time.
    Send-PowerDbgCommand "!runaway 1"
    
    $hasCommandSucceeded = Has-PowerDbgCommandSucceeded

    if($false -eq $hasCommandSucceeded)
    {
        throw "This dump has no threads information!"
    }
    
    # Gets the array of user mode time.
    $arrayOfUserTime = Convert-PowerDbgRUNAWAYtoArray
     
    # Let's get Kernel time.
    # It's not necessary to validate the command output because we already did that.
    Send-PowerDbgCommand "!runaway 2"
    
    # Parses to get a hash table.
    Parse-PowerDbgRUNAWAY
    
    # Gets a hash table from the CSV file created with the cmdlet above.
    $kernelTime = Convert-PowerDbgCSVtoHashTable
    
    # Analyze the call stack for each thread to know what they are doing.
    $arrayOfClassification = Classify-PowerDbgThreads
   
    # Simple header.
    write-Host "Threads sorted by User Time...`n" -foreground Green -background Black      
    write-Host "Thread Number`tUser Time`tKernel Time`t`tActivity" -foreground Green -background Black            
          
    # Displays information to user.
    for([System.Int32] $i = 0; $i -lt [System.Math]::truncate(($arrayOfUserTime.Count / 2)); $i++)
    {
        # If unknown activity, put it in red.
        if($arrayOfClassification[$arrayOfUserTime[$i, 0]] -eq $global:g_unknownSymbol)
        {
            write-Host "  " $arrayOfUserTime[$i, 0] "`t`t" $arrayOfUserTime[$i, 1] "`t" $kernelTime[$arrayOfUserTime[$i, 0]] "`t" $global:g_whatThreadIsDoing[$arrayOfClassification[$arrayOfUserTime[$i, 0]]] -foreground Red -background Black          
        }
        else
        {
            write-Host "  " $arrayOfUserTime[$i, 0] "`t`t" $arrayOfUserTime[$i, 1] "`t" $kernelTime[$arrayOfUserTime[$i, 0]] "`t" $global:g_whatThreadIsDoing[$arrayOfClassification[$arrayOfUserTime[$i, 0]]] -foreground Green -background Black          
        }
    }
    Send-PowerDbgComment "Analyze-PowerDbgThreads finished execution."
}




########################################################################################################
# Function:    Parse-PowerDbgPRINTEXCEPTION
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!PrintException" command using a hash table. The output 
#              is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
#              Note: For this version the fields being considered are: 
#              Exception object:
#              Exception type:
#              Message:
#              InnerException:
#              HResult:
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgPRINTEXCEPTION()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # \s+   --> Scans for one or more spaces.
    # (\S+) --> Gets one or more chars/digits/numbers without spaces.
    # \s+   --> Scans for one or more spaces.
    # (\w+) --> Gets one or more chars.
    # .+    --> Scans for one or more chars (any char except for new line).
    # \:    --> Scans for the ':' char.
    # \s+   --> Scans for one or more spaces.
    # (.+.) --> Gets the entire remainder string including the spaces.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
       if($line -match "((?<key>(^Exception object:)|(^Exception type:)|(^Message:)|(^InnerException:)|(^HResult:))\s+(?<value>.+))")
       {
           $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
       } 
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}



########################################################################################################
# Function:    Parse-PowerDbgDD-L1
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "dd <address> L1" or "dd poi(<address>) L1" command using a hash table. 
#              The key is the address and the output from the command is the value.
#              The output is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgDD-L1()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")
    
    # This is to be able to read each line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
        if($line -match "((?<key>(^\w+))\s+(?<value>(\w+)))")
        {
            $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])            
        }   
    }  
   
    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"   
}





########################################################################################################
# Function:    Parse-PowerDbgGCHANDLELEAKS
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!GCHandleLeaks" command using a hash table. The key is the handle and 
#              the value is the object corresponding to the handle. The output is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgGCHANDLELEAKS()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder    = $builder.AppendLine("key,value")
    $foundLeak  = $false
    [int] $i    = 0
    [System.Collections.ArrayList] $arrHandles = @()
    
    # This is to be able to read each line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
        # If leak was found, map all handles that are leaking.
        # Valid output from 'g_fileCommandOutput' may be:
        # 022014d0        022014d4        022014d8        022014dc
        # 022014e0        022014e4        022014ec
        # 022014f4        022014f8
        # 022014f4
        # The expression below should be optmized.
        if($foundLeak -and (($line -match "((?<key>(^\w+))\t(?<key2>(\w+))\t(?<key3>(\w+))\t(?<key4>(\w+)))") -or
                            ($line -match "((?<key>(^\w+))\t(?<key2>(\w+))\t(?<key3>(\w+))\t)") -or
                            ($line -match "((?<key>(^\w+))\t(?<key2>(\w+))\t)") -or
                            ($line -match "((?<key>(^\w+))\t)")))
        {
            # Save the keys just if they have value, ignoring spaces or tabs.
            # We use a temporary variable to not display the results.
            if($matches["key"].Length -ge 1)
            {
                $a= $arrHandles.Add($matches["key"])
            }

            if($matches["key2"].Length -ge 1)
            {
                $a = $arrHandles.Add($matches["key2"])
            }
            
            if($matches["key3"].Length -ge 1)
            {          
                $a = $arrHandles.Add($matches["key3"])
            }
            
            if($matches["key4"].Length -ge 1)
            {         
                $a = $arrHandles.Add($matches["key4"])
            }
        }   
        elseif(($foundLeak -eq $false) -and ($line -match "(?<key>(^Didn))") )
        {
            # Verify is there are handles leaking. We use part of the sentence "Didn't find..." to check if there are leaks.        
            $foundLeak = $true
        }
        elseif($line -match "((?<key>(^All handles found.)))")
        {
            break
        }
    }  

    # Now we scan each handle, get the corresponding object and save it in a CSV file.
    for($i = 0; $i -lt $arrHandles.Count; $i++)
    {       
        [string] $handle = $arrHandles[$i]  
        
        Send-PowerDbgCommand "dd $handle L1"   
               
        Parse-PowerDbgDD-L1
        
        $content = Convert-PowerDbgCSVToHashTable

        $builder = $builder.AppendLine($handle + $global:g_CSVDelimiter + $content["$handle"])
    }
    
    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
    
}



########################################################################################################
# Function:    Parse-PowerDbgDUMPOBJ
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from "!DumpObj" command using a hash table. 
#              The assembly path and file name are saved with the key name 'Assembly:'.
#              If the object is invalid the Name: will have the string "Invalid object". You may want to check
#              this string to make sure you got valid data.
#
#              The key are the fields or Method Table and the value is the corresponding value.
#              The output is saved into the file POWERDBG-PARSED.LOG 
#              All Parse functions should use the same outputfile.
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable() does that.
#
#              Attention! This version maps the fields below "Fields:" using the MethodTable as key and Value as value.
#              The problem with this approach is that the same MethodTable may appear more than once. If it happens
#              just the latest MethodTable and Value will be saved in the CSV file.
#              Based on users' feedback I may change this approach in the future. (and break compatibility :))
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgDUMPOBJ()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")
    
    # This is to be able to read each line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
        if($line -match "((?<key>(^Name:)|(^MethodTable:)|(^EEClass:)|(^Size:)|(^String:))\s+(?<value>(.+)))")
        {              
            $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])            
        }
        elseif($line -match "((?<key>(^\w+))\s+\w+\s+\w+\s+\w+\.\w+\s+\w+\s+\w+\s+(?<value>(\w+)))")
        {
            $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])                    
        }
        elseif($line -match "((?<key>(^\s\())(?<value>(.+))\))")
        {
            # The assembly name and path is saved under the key 'Assembly:'.
            $builder = $builder.AppendLine("Assembly:" + $global:g_CSVDelimiter + $matches["value"])                            
        }
        elseif($line -match "((?<key>(Note: this object has an invalid CLASS field)|(^Invalid object)))")
        {
            # Object is invalid.
            $builder = $builder.AppendLine("Name:" + $global:g_CSVDelimiter + "Invalid object")                            
        }   
    }  
   
    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"   
}





########################################################################################################
# Function:    Load-PowerDbgSymbols
#
# Parameters:  [string] <$symbols> 
#              Directory to store the symbols and symbols path.                       
#
# Return:      Nothing.
#
# Purpose:     Load symbols.
#
# Usage:       Load-PowerDbgSymbols "SRV*c:\PUBLICSYMBOLS*http://msdl.microsoft.com/download/symbols"
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Load-PowerDbgSymbols([string] $symbols = $(throw "Error! You must provide the directory to store symbols and the symbols path."))
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    Send-PowerDbgCommand ".sympath $symbols"
}




########################################################################################################
# Function:    Parse-PowerDbgASPXPAGES
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!ASPXPages" command and saves it into the file POWERDBG-PARSED.LOG 
#              You can easily map the POWERDBG-PARSED.LOG to a hash table.
#              Convert-PowerDbgCSVtoHashTable does that.
#
#              Note: For this version the fields being considered are: 
#              HttpContext    Timeout+Completed+Running+ThreadId+ReturnCode+Verb+RequestPath+QueryString
#
#              HttpContext is the key and all other fields together are the value.
#         
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgASPXPAGES()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # (\S+) --> Scans for any character except for white spaces.
    # \s+   --> Scans for one or more spaces.
    # (.+)  --> Scans for one or more chars (any char except for new line).
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
       if($line -match "((?<key>(0x\S+))\s+(?<value>(.+)))")
       {
           $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
       } 
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}




########################################################################################################
# Function:    Parse-PowerDbgCLRSTACK
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!clrstack" command or "~* e !clrstack" and saves it into the file POWERDBG-PARSED.LOG 
#              You can easily map the POWERDBG-PARSED.LOG to a hash table using Convert-PowerDbgCSVtoHashTable.
#              The key is the thread number and the value is the call stack separated by '$global:g_frameDelimiter'.

# Attention!   Commas "," are replaced for ";" to avoid confusing with the comma used by the CSV file.
#              If you use this cmdlet to parse the output from "~* e !clrstack" the threads not running managed code are ignored.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgCLRSTACK()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")
    $key     = ""
    
    # \S+   --> Searches for any character except for white spaces.
    # \s+   --> Searches for one or more spaces.
    # (\S+) --> Searches for any character except white space and save it.
    # (.+)  --> Saves the remainder characters until new line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
    
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    {         
        # Verifies if this is the line that has the thread number.
        if($line -match "^OS Thread Id:.+\((?<key>(\d+))\)")
        {
            $key = $matches["key"]            
        }
        elseif($line.Contains("Unable to walk the managed stack") -or $line.StartsWith("ESP") -or $line.StartsWith("managed thread") -or $line.StartsWith("the process") ) 
        {  
            continue  # Not running managed code or the line has garbage. 
        }
        elseif($line -match "(?<value>(.+))")
        {
            # If content is an empty string ignore it.
            if(($matches["value"] -ne "") -and ($matches["value"] -ne " "))
            {
                # We save the key just the first time.
                if($key -ne "")
                {
                    # Each new CSV line should start from the beginning not as a continuation from the previous line.
                    $builder = $builder.AppendLine("")        
                    $builder = $builder.Append($key + $global:g_CSVDelimiter)                                           
                }
            
                $builder = $builder.Append($matches["value"].Replace(",", ";") + $global:g_frameDelimiter)             
            }
            
            $key = ""            
        }   
        
        else
        {
            $key = ""
            continue # If this is not our pattern ignore it.
        }
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}




########################################################################################################
# Function:    Parse-PowerDbgTHREADS
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!threads" command and saves it into the file POWERDBG-PARSED.LOG 
#              You can easily map the POWERDBG-PARSED.LOG to a hash table using Convert-PowerDbgCSVtoHashTable.
#
#              The following fields are extracted:
#              Thread Number                                                 - Key
#              ID+OSID+ThreadOBJ+State+GC+Context+Domain+Count+APT+Exception - Value
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgTHREADS()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")

    # ^\s*  --> Searches for 0 or more spaces from the beginning of the line.
    # (\d+) --> Searches for decimal characters and save them.
    # \s+   --> Searches for 1 or more spaces.
    # (.+)  --> Saves the remainder characters until new line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    { 
        if($line -match "^\s*((?<key>(\d+))\s+(?<value>(.+)))")
        {
            $builder = $builder.AppendLine($matches["key"] + $global:g_CSVDelimiter + $matches["value"])    
        }        
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}






########################################################################################################
# Function:    Parse-PowerDbgDSO
#
# Parameters:  None.
#                                         
# Return:      Nothing.
#
# Purpose:     Maps the output from the "!dso" command and saves it into the file POWERDBG-PARSED.LOG 
#              You can easily map the POWERDBG-PARSED.LOG to a hash table using Convert-PowerDbgCSVtoHashTable.
#              The Thread Number is the key and the stack is the value.
#
#              Attention! Commas are replaced by ";" and '$global:g_FrameDelimiter' is used to separate frames.
#
# Changes History:
#
# Roberto Alexis Farah
# All my functions are provided "AS IS" with no warranties, and confer no rights. 
########################################################################################################
function Parse-PowerDbgDSO()
{
    set-psdebug -strict
    $ErrorActionPreference = "stop"
    trap {"Error message: $_"}

    # Extract output removing commands.
    $builder = New-Object System.Text.StringBuilder

    # Title for the CSV fields.
    $builder = $builder.AppendLine("key,value")
    $key     = ""
    
    # ^\S+ --> Searches any characters that stars with char or number from the beginning of the line.
    # \s*   --> Searches for one or more spaces.
    # (\S+) --> Searches for 1 or more spaces.
    # (.+)  --> Saves the remainder characters until new line.
    $stringReader = [System.IO.StringReader] $global:g_commandOutput
        
    # Scan the symbols for each thread, line by line.
    while(($line = $stringReader.ReadLine()) -ne $null)
    {
        # Verifies if this is the line that has the thread number.
        if($line -match "^OS Thread Id:.+\((?<key>(\d+))\)")
        {
            $key = $matches["key"]                
        }    
        elseif($line.StartsWith("ESP/REG  Object   Name"))
        {
            continue
        }
        elseif($line -match "^\S+\s*((?<key>(\S+))\s+(?<value>(.+)))")
        {          
            # If content is an empty string ignore it.
            if(([string] $matches["value"] -eq "") -or ([string] $matches["value"] -eq " "))
            {              
                continue
            }
                
            # We save the key just the first time.
            if($key -ne "")
            {
                # Each new CSV line should start from the beginning not as a continuation from the previous line.
                $builder = $builder.AppendLine("")        
                $builder = $builder.Append($key + $global:g_CSVDelimiter)                                           
            }

            $builder = $builder.Append($matches["value"].Replace(",", ";") + $global:g_frameDelimiter)             
              
            $key = ""          
        }
    }  

    # Send output to our default file.
    out-file -filepath $global:g_fileParsedOutput -inputobject "$builder"
}



Export-ModuleMember -Function Connect-Windbg 
Export-ModuleMember -Function Disconnect-Windbg 
Export-ModuleMember -Function Invoke-WindbgCommand 
Export-ModuleMember -Function Connect-Dbg
Export-ModuleMember -Function Load-PowerDbgSymbols 
Export-ModuleMember -Function Parse-PowerDbgASPXPAGES
Export-ModuleMember -Function Parse-PowerDbgCLRSTACK
Export-ModuleMember -Function Parse-PowerDbgTHREADS
Export-ModuleMember -Function Parse-PowerDbgDSO
Export-ModuleMember -Function Send-PowerDbgCommand
Export-ModuleMember -Function Parse-PowerDbgDT 
Export-ModuleMember -Function Convert-PowerDbgCSVToHashTable
Export-ModuleMember -Function Send-PowerDbgDML 
Export-ModuleMember -Function Parse-PowerDbgNAME2EE
Export-ModuleMember -Function Parse-PowerDbgDUMPMD
Export-ModuleMember -Function Parse-PowerDbgDUMPMODULE
Export-ModuleMember -Function Parse-PowerDbgLMI
Export-ModuleMember -Function Has-PowerDbgCOMMANDSUCCEEDED
Export-ModuleMember -Function Send-PowerDbgComment
Export-ModuleMember -Function Parse-PowerDbgVERTARGET
Export-ModuleMember -Function Parse-PowerDbgRUNAWAY
Export-ModuleMember -Function Convert-PowerDbgRUNAWAYtoArray
Export-ModuleMember -Function Parse-PowerDbgK
Export-ModuleMember -Function Parse-PowerDbgSymbolsFromK
Export-ModuleMember -Function Parse-PowerDbgLM1M
Export-ModuleMember -Function Classify-PowerDbgThreads
Export-ModuleMember -Function Analyze-PowerDbgThreads
Export-ModuleMember -Function Parse-PowerDbgPRINTEXCEPTION
Export-ModuleMember -Function Parse-PowerDbgDD-L1
Export-ModuleMember -Function Parse-PowerDbgGCHANDLELEAKS
Export-ModuleMember -Function Parse-PowerDbgDUMPOBJ

